package apibasej.basic.config;

import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;

import apibasej.basic.exception.APIBaseRuntimeException;
import apibasej.basic.misc.UtilTrace;

public class ConfigManager implements ConfigConsumerInterface{

	private static HashMap<Prop<?>,Object> defaultPropertiesForAll = null;
	
	//private static HashMap<String,HashMap<Prop<?>,Object>> propertiesForClasses = null;
	// caso tenha um grupo de objetos que usem a mesma configuração, a identificação será uma string
	// por padrão o grupo é o nome da classe, mas pode ser setado um nome de pacote ou outro qualquer
	private static HashMap<String,HashMap<Prop<?>,Object>> propertiesForGroups = null;
	
	private HashMap<Prop<?>,Object> propertiesForThisInstance = null;
	
	private String groupId = null;// caso tenha alguma identificação para grupo que não seja a classe, o id do grupo deve ser setado ...
	
	
	// precisa ficar setando toda vez, para caso tenha sido alterado ...
	private static void configDefaults(Locale l, TimeZone t, String charset){
		try{
			if(l!=null) Locale.setDefault(l);
			if(t!=null) TimeZone.setDefault(t);// precisa para corrigir horário para o new java.util.Date
			if(charset!=null) System.setProperty("file.encoding", charset);
		}catch (Throwable e) {
			String m = "ERRO AO SETAR TIME ZONE, LOCALE E CHARSET DEFAULT!";
			logDefAll(Level.SEVERE, m, e);
		}
	}
	
	static{
		configDefaults(LOCALE.getDefaultValue(), TIME_ZONE.getDefaultValue(), CHARSET.getDefaultValue());
	}
	
	public ConfigManager() {
		this.groupId = getClass().getName();
	}
	public ConfigManager(String groupId){
		this.groupId = groupId;
	}
	
	//??? pode confundir! usar grupo se forem compartilher mesmas propriedades
	//public ConfigManager(ConfigManager sourceToReplaceThis){
	//	replaceThisProps(sourceToReplaceThis);
	//}
	
	public String getGroupId() {
		if(groupId==null){
			return getClass().getName();
		}else{
			return groupId;
		}
	}
	public void setGroupId(String groupId) {
		this.groupId = groupId;
	}
	
	public Date getDateNow(){
		
		// precisa ficar setando toda vez, para caso tenha sido alterado ...
		configDefaults(getProp(LOCALE), getProp(TIME_ZONE), null);
		
		// o time millis é sempre GMT 0, a transformação fica por conta do Date
		return new Date(System.currentTimeMillis()+getProp(TIME_CORRECTION));
	}
	
	public static void configDefaultsTimeZoneAndLocale(){
		// precisa ficar setando toda vez, para caso tenha sido alterado ...
		configDefaults(getPropDefaultForAll(LOCALE),getPropDefaultForAll(TIME_ZONE),null); 
	}
	
	public static Date getDateNowSingl(){
		
		// precisa ficar setando toda vez, para caso tenha sido alterado ...
		configDefaultsTimeZoneAndLocale();
		
		// o time millis é sempre GMT 0, a transformação fica por conta do Date
		return new Date(System.currentTimeMillis()+getPropDefaultForAll(TIME_CORRECTION));
	}
	
	@SuppressWarnings("unchecked")
	public static final <T> T getPropDefaultForAll(Prop<T> prop){
		if(defaultPropertiesForAll!=null && defaultPropertiesForAll.containsKey(prop)){
			T val =(T)defaultPropertiesForAll.get(prop);
			if(!prop.allowsNull() && val==null){// só cai aqui se setar valor null para esta prop default
				throw new APIBaseRuntimeException("Propertie default is null: "+prop);
			}else{
				return val;
			}
		}
		if(!prop.allowsNull() && prop.getDefaultValue()==null){
			throw new APIBaseRuntimeException("Property was not set: "+prop);
		}else{
			return prop.getDefaultValue();
		}
	}
	
	@SuppressWarnings("unchecked")
	public static final <T> T getPropForGroup(Prop<T> prop, String groupId){
		validadeGroupId(groupId);
		if(propertiesForGroups!=null && propertiesForGroups.containsKey(groupId)){
			HashMap<Prop<?>,?> propsForThisGroup = propertiesForGroups.get(groupId);
			if(propsForThisGroup!=null && propsForThisGroup.containsKey(prop)){
				T val = (T)propsForThisGroup.get(prop);
				if(!prop.allowsNull() && val==null){// só cai aqui se setar valor null para esta prop para esta classe
					throw new APIBaseRuntimeException("Propertie for group is null. prop="+prop+", groupId="+groupId);
				}else{
					return val;
				}
			}
		}
		return getPropDefaultForAll(prop); // se não achar prop para o grupo retorna a prop default para todos
	}
	
	// neste caso o nome da classe será o grupo
	public static final <T> T getPropForGroup(Prop<T> prop, Class<?> clazz){
		return getPropForGroup(prop, clazz.getName());
	}
	
	public final <T> T getPropForThisGroup(Prop<T> prop){
		return getPropForGroup(prop, getGroupId());
	}
	
	@Override
	@SuppressWarnings("unchecked")
	public final <T> T getProp(Prop<T> prop) {
		if(propertiesForThisInstance!=null && propertiesForThisInstance.containsKey(prop)){
			T val =(T)propertiesForThisInstance.get(prop);
			if(!prop.allowsNull() && val==null){// só cai aqui se setar valor null para esta prop default
				throw new APIBaseRuntimeException("Propertie for this instance is null: "+prop);
			}else{
				return val;
			}
		}
		return getPropForThisGroup(prop); // se não tiver setado específico para esta instância tenta primeiro para esta classe depois para todas ...
	}
	
	// synchronized para não ter risco de duas threads simultâneas criarem uma por cima da outra
	private static synchronized final void createDefaultPropertiesForAll(){
		if(defaultPropertiesForAll==null){
			defaultPropertiesForAll = new HashMap<Prop<?>,Object>();
		}
	}
	// synchronized para não ter risco de duas threads simultâneas criarem uma por cima da outra
	private static synchronized final void createPropertiesForGroups(){
		if(propertiesForGroups==null){
			propertiesForGroups = new HashMap<String,HashMap<Prop<?>,Object>>();
		}
	}
	
	public static final <T> void setPropDefaultForAll(Prop<T> prop, T value){
		if(defaultPropertiesForAll==null) {
			createDefaultPropertiesForAll();
		}
		defaultPropertiesForAll.put(prop, value);
	}

	public static final <T> void setPropForGroup(Prop<T> prop, T value, String groupId){
		if(propertiesForGroups==null) {
			createPropertiesForGroups();
		}
		validadeGroupId(groupId);
		HashMap<Prop<?>,Object> propsForGroup = propertiesForGroups.get(groupId);
		if(propsForGroup==null){
			propsForGroup = new HashMap<Prop<?>,Object>();
			propertiesForGroups.put(groupId, propsForGroup);
		}
		propsForGroup.put(prop, value);
	}
	public final <T> void setPropForThisGroup(Prop<T> prop, T value){
		setPropForGroup(prop, value, getGroupId());
	}
	public final <T> void setPropForThisInstance(Prop<T> prop, T value){
		if(propertiesForThisInstance==null) {
			propertiesForThisInstance = new HashMap<Prop<?>,Object>();
		}
		propertiesForThisInstance.put(prop, value);
	}
	
	//??? pode confundir! usar grupo se forem compartilher mesmas propriedades
	//public void replaceThisProps(ConfigManager propsSource){
	//	this.propertiesForThisInstance = propsSource.propertiesForThisInstance;
	//}

	private static void validadeGroupId(String groupId){
		if(groupId==null){
			throw new NullPointerException("groupId cannot be null!");
		}
	}
	
	
	
	
	private static void logPriv(Level l, String msg, Throwable t, Level levelConfig, LogInterface logImpl){
		if(logImpl==null){//não deve ocorrer
			throw new NullPointerException("LogInterface cannot be null!");
		}
		if(l!=null && levelConfig.intValue()<=l.intValue()){
			
			//se veio de dentro de uma mesma impl do logger
			if(UtilTrace.isRecursiveCall(t)){
				/* teria que validar se este logger é console e se veio também de uma console
				if(logImpl.getClass().getName().equals(LogConsole.class.getName())){
					//pode ocorrer se tiver uma outra impl do System.out (System.setOut(...)) que tente registrar algum log por aqui
					throw new IllegalStateException("#!#!# SEVERE LOG ERROR: log in the console can not have recursive calls! #!#!#");
				}else{
					System.err.println("### ConfigManager.logPriv(...): RECURSIVE CALL FOR: MSG="+msg+"; ERR="+t+" ###");
					logImpl = new LogConsole();// para usar somente o log na console !!!
				}*/
				System.err.println("### ConfigManager.logPriv(...): RECURSIVE CALL FOR: MSG="+msg+"; ERR="+t+" ###");
				logImpl = new LogConsole();// para usar somente o log na console !!!
			}
			
			logImpl.logImpl(l, msg, t);
		}
	}
	public static void logDefAll(Level l, String msg, Throwable t){
		logPriv(l, msg, t, getPropDefaultForAll(LOG_LEVEL), getPropDefaultForAll(LOGGER_IMPL));
	}
	public void logDef(Level l, String msg, Throwable t){
		logPriv(l, msg, t, getProp(LOG_LEVEL), getProp(LOGGER_IMPL));
	}
}
